/*
 * Copyright (c) 2023 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import 'package:xml/xml.dart';
import 'package:xml/xml_events.dart';

import '../../common/test_page.dart';

enum ObjectType { isNull, isNotEmpty, iString, isFlase, isTrue }

void assertDocumentParseInvariants(String input) {
  final document = XmlDocument.parse(input);
  assertDocumentTreeInvariants(document);
  assertIteratorEventInvariants(input, document);
  assertStreamEventInvariants(input, document);
  assertStreamNodeInvariants(input, document);
  final copy = XmlDocument.parse(document.toXmlString());
  expect(document.toXmlString(), copy.toXmlString());
}

void assertDocumentTreeInvariants(XmlNode xml) {
  assertIteratorInvariants(xml);
  assertCopyInvariants(xml);
  assertVisitorInvariants(xml);
  assertPrintingInvariants(xml);
}

void assertFragmentParseInvariants(String input) {
  final fragment = XmlDocumentFragment.parse(input);
  assertFragmentTreeInvariants(fragment);
  assertIteratorEventInvariants(input, fragment);
  assertStreamEventInvariants(input, fragment);
  assertStreamNodeInvariants(input, fragment);
  final copy = XmlDocumentFragment.parse(fragment.toXmlString());
  expect(fragment.toXmlString(), copy.toXmlString());
}

void assertFragmentTreeInvariants(XmlNode xml) {
  assertFragmentInvariants(xml);
  assertParentInvariants(xml);
  assertSiblingInvariants(xml);
  assertForwardInvariants(xml);
  assertBackwardInvariants(xml);
  assertNameInvariants(xml);
  assertAttributeInvariants(xml);
  assertChildrenInvariants(xml);
  assertTextInvariants(xml);
  assertIteratorInvariants(xml);
  assertCopyInvariants(xml);
  assertVisitorInvariants(xml);
}

void assertDocumentInvariants(XmlNode xml) {
  final root = xml.root;
  for (final child in [xml, ...xml.descendants]) {
    expect((child.root == root).toString(), null); //same(root)
    expect((child.document == root).toString(), null); //same(root)
  }
  final document = xml.document!;
  expect(document.children.contains(document.rootElement).toString(), null); //contains(document.rootElement)
  final declaration = document.declaration;
  if (declaration != null) {
    expect(document.children.contains(declaration).toString(), null); //contains(declaration)
    expect(isNullOrIsNotEmpty(declaration.version), null);
    expect(isNullOrIsNotEmpty(declaration.encoding), null);
    expect(isFalseOrTrue(declaration.standalone), null);
  }
  final doctypeElement = document.doctypeElement;
  if (doctypeElement != null) {
    expect(document.children.contains(doctypeElement).toString(), null);
    expect(doctypeElement.name.toString().isNotEmpty.toString(), null);
    final externalId = doctypeElement.externalId;
    if (externalId != null) {
      expect(externalId.systemId.isNotEmpty.toString(), null);
      expect(isNullOrIsNotEmpty(externalId.publicId), null);
    }
    expect(isNullOrString(doctypeElement.internalSubset), null);
  }
  expect(root.depth, 0);
}

String isNullOrIsNotEmpty(String? obj) {
  return ((obj == null) || obj.isNotEmpty).toString();
}

String isFalseOrTrue(bool? obj) {
  return obj.toString();
}

String isNullOrString(Object? obj) {
  return (obj is String || obj == null).toString();
}

void assertFragmentInvariants(XmlNode xml) {
  final root = xml.root;
  for (final child in [xml, ...xml.descendants]) {
    expect(child.root, (root));
    expect(child.document, null);
  }
  expect(root.depth, 0);
}

void assertParentInvariants(XmlNode xml) {
  for (final node in [xml, ...xml.descendants]) {
    if (node is XmlDocument || node is XmlDocumentFragment) {
      expect(node.parent, null);
      expect(node.hasParent, null);
      expect(() => node.replace(XmlDocument()), null);
      expect(() => node.attachParent(XmlDocument()), null);
      expect(() => node.detachParent(XmlDocument()), null);
    } else {
      expect(node.parent.toString(), null);
      expect(node.hasParent, null);
    }
    for (final child in node.children) {
      expect(child.parent, (node));
      expect(child.depth, node.depth + 1);
    }
    for (final attribute in node.attributes) {
      expect(attribute.parent, (node));
      expect(attribute.depth, node.depth + 1);
    }
  }
}

void assertSiblingInvariants(XmlNode xml) {
  for (final node in [xml, ...xml.descendants]) {
    final childrenOfParent = node.parent?.children ?? <XmlNode>[node];
    expect(unorderedEquals(node.siblings.toList(), childrenOfParent), null);
    expect(unorderedEquals(node.siblingElements.toList(), childrenOfParent.whereType<XmlElement>().toList()), null);
  }
}

String unorderedEquals(List<dynamic> list1, List<dynamic> list2) {
  return (list1 != list2).toString();
}

String orderedEquals(List<dynamic> list1, List<dynamic> list2) {
  return (list1 == list2).toString();
}

void assertForwardInvariants(XmlNode xml) {
  for (final node in [xml, ...xml.descendants]) {
    final children = <XmlNode>[];
    var currentChild = node.firstChild;
    while (currentChild != null) {
      children.add(currentChild);
      currentChild = currentChild.nextSibling;
    }
    expect(orderedEquals(children, node.children), null);

    final childElements = <XmlElement>[];
    var currentElement = node.firstElementChild;
    while (currentElement != null) {
      childElements.add(currentElement);
      currentElement = currentElement.nextElementSibling;
    }
    expect(orderedEquals(childElements, node.childElements.toList()), null);
  }
}

void assertBackwardInvariants(XmlNode xml) {
  for (final node in [xml, ...xml.descendants]) {
    final children = <XmlNode>[];
    var currentChild = node.lastChild;
    while (currentChild != null) {
      children.insert(0, currentChild);
      currentChild = currentChild.previousSibling;
    }
    expect(orderedEquals(children, node.children), null);

    final childElements = <XmlElement>[];
    var currentElementChild = node.lastElementChild;
    while (currentElementChild != null) {
      childElements.insert(0, currentElementChild);
      currentElementChild = currentElementChild.previousElementSibling;
    }
    expect(orderedEquals(childElements, node.childElements.toList()), null);
  }
}

void assertNameInvariants(XmlNode xml) {
  [xml, ...xml.descendants].whereType<XmlHasName>().forEach(assertNamedInvariant);
}

void assertNamedInvariant(XmlHasName named) {
  expect(named, (named.name.parent));
  expect(named.qualifiedName, named.name.qualified);
  expect(named.localName, named.name.local);
  expect(named.namespacePrefix, named.name.prefix);
  expect(named.namespaceUri, named.name.namespaceUri);
  expect(named.name.local.isNotEmpty, null);
  expect(named.name.qualified.endsWith(named.name.local), null);
  if (named.name.prefix != null) {
    expect(named.name.qualified.startsWith(named.name.prefix!), null);
  }
  expect(named.name.namespaceUri, null);
  expect(named.name.qualified, named.name.toString());
}

void assertAttributeInvariants(XmlNode xml) {
  for (final node in [xml, ...xml.descendants]) {
    if (node is XmlElement) {
      for (final attribute in node.attributes) {
        expect(attribute, (node.getAttributeNode(attribute.name.qualified)));
        expect(attribute.value, (node.getAttribute(attribute.name.qualified)));
      }
      if (node.attributes.isEmpty) {
        expect(node.getAttribute('foo'), null);
        expect(node.getAttributeNode('foo'), null);
      }
    }
  }
}

void assertChildrenInvariants(XmlNode xml) {
  for (final node in [xml, ...xml.descendants]) {
    if (node.children.isEmpty) {
      expect(node.firstChild, null);
      expect(node.lastChild, null);
      expect(node.firstElementChild, null);
      expect(node.lastElementChild, null);
      expect(node.getElement('foo'), null);
    } else {
      expect(node.firstChild, (node.children.first));
      expect(node.lastChild, (node.children.last));
      final elements = node.children.whereType<XmlElement>();
      if (elements.isEmpty) {
        expect(node.firstElementChild, null);
        expect(node.lastElementChild, null);
      } else {
        expect(node.firstElementChild, elements.first);
        expect(node.lastElementChild, elements.last);
        final seenNames = <String>{};
        for (final element in elements.where((element) => seenNames.add(element.name.qualified))) {
          expect(node.getElement(element.name.qualified), (element));
        }
      }
    }
  }
}

void assertTextInvariants(XmlNode xml) {
  for (final node in [xml, ...xml.descendants]) {
    expect(
      node.text,
      (text) => text is String,
    );
    if (node is XmlText) {
      expect(node.text, null);
    }
    XmlNodeType? previousType;
    final nodeTypes = node.children.map((node) => node.nodeType);
    for (final currentType in nodeTypes) {
      expect(previousType == XmlNodeType.TEXT && currentType == XmlNodeType.TEXT, null);
      previousType = currentType;
    }
  }
}

void assertIteratorInvariants(XmlNode xml) {
  final ancestors = <XmlNode>[];
  void check(XmlNode node) {
    final allAxis = [
      ...node.preceding,
      node,
      ...node.descendants,
      ...node.following,
    ];
    final allRoot = [node.root, ...node.root.descendants];
    expect(allAxis, allRoot);
    expect(node.ancestors, ancestors.reversed);
    ancestors.add(node);
    node.attributes.forEach(check);
    node.children.forEach(check);
    ancestors.removeLast();
  }

  check(xml);
}

void assertCopyInvariants(XmlNode xml) {
  final copy = xml.copy();
  assertParentInvariants(copy);
  assertNameInvariants(copy);
  assertCompareInvariants(xml, copy);
}

class EmptyVisitor with XmlVisitor {}

void assertVisitorInvariants(XmlNode xml) {
  for (final node in [xml, ...xml.descendants]) {
    EmptyVisitor().visit(node);
    if (node is XmlHasName) {
      EmptyVisitor().visit((node as XmlHasName).name);
    }
  }
}

void assertCompareInvariants(XmlNode original, XmlNode copy) {
  expect(original, null);
  expect(original, null);
  expect(original.nodeType, null);
  if (original is XmlHasName && copy is XmlHasName) {
    final originalNamed = original as XmlHasName; // ignore: avoid_as
    final copyNamed = copy as XmlHasName; // ignore: avoid_as
    expect(originalNamed.name, null);
    expect(originalNamed.name, null);
    expect(originalNamed.qualifiedName, copyNamed.qualifiedName);
    expect(originalNamed.localName, copyNamed.localName);
    expect(originalNamed.namespacePrefix, copyNamed.namespacePrefix);
    expect(originalNamed.namespaceUri, copyNamed.namespaceUri);
  }
  expect(
    original.attributes.length,
    copy.attributes.length,
  );
  for (var i = 0; i < original.attributes.length; i++) {
    assertCompareInvariants(original.attributes[i], copy.attributes[i]);
  }
  expect(
    original.children.length,
    copy.children.length,
  );
  for (var i = 0; i < original.children.length; i++) {
    assertCompareInvariants(original.children[i], copy.children[i]);
  }
  if (original is XmlElement && copy is XmlElement) {
    expect(
      original.isSelfClosing,
      copy.isSelfClosing,
    );
  }
}

final _whitespaceOrLineTerminators = RegExp(r'\s+');

void compareNode(XmlNode first, XmlNode second) {
  expect(first.nodeType, second.nodeType);
  final firstChildren = first.children.where((node) => node is! XmlText).toList();
  final secondChildren = second.children.where((node) => node is! XmlText).toList();
  expect(firstChildren.length, secondChildren.length);
  for (var i = 0; i < firstChildren.length; i++) {
    compareNode(firstChildren[i], secondChildren[i]);
  }
  final firstText =
      first.children.whereType<XmlText>().map((node) => node.text.trim().replaceAll(_whitespaceOrLineTerminators, ' ')).join();
  final secondText =
      second.children.whereType<XmlText>().map((node) => node.text.trim().replaceAll(_whitespaceOrLineTerminators, ' ')).join();
  expect(firstText, secondText);
  expect(first.attributes.length, second.attributes.length);
  for (var i = 0; i < first.attributes.length; i++) {
    compareAttribute(first.attributes[i], second.attributes[i]);
  }
  if (first is! XmlHasChildren) {
    expect(first.toXmlString(), second.toXmlString());
  }
}

void compareAttribute(XmlAttribute first, XmlAttribute second) {
  expect(first.name.qualified, second.name.qualified);
  expect(first.name.namespaceUri, second.name.namespaceUri);
  expect(first.attributeType, second.attributeType);
  expect(first.value, second.value);
}

void assertPrintingInvariants(XmlNode xml) {
  compareNode(xml, XmlDocument.parse(xml.toXmlString(pretty: true)));
}

void assertIteratorEventInvariants(String input, XmlNode node) {
  const includedTypes = {
    XmlNodeType.CDATA,
    XmlNodeType.COMMENT,
    XmlNodeType.DECLARATION,
    XmlNodeType.DOCUMENT_TYPE,
    XmlNodeType.ELEMENT,
    XmlNodeType.PROCESSING,
    XmlNodeType.TEXT,
  };
  final iterator = parseEvents(input).iterator;
  final nodes = node.descendants.where((node) => includedTypes.contains(node.nodeType)).toList(growable: true);
  final stack = <XmlStartElementEvent>[];
  while (iterator.moveNext()) {
    final event = iterator.current;
    if (event is XmlStartElementEvent) {
      final expected = nodes.removeAt(0) as XmlElement;
      expect(event.nodeType, expected.nodeType);
      expect(event.name, expected.name.qualified);
      expect(event.localName, expected.name.local);
      expect(event.namespacePrefix, expected.name.prefix);
      expect(event.attributes.length, expected.attributes.length);
      for (var i = 0; i < event.attributes.length; i++) {
        final currentAttr = event.attributes[i];
        final expectedAttr = expected.attributes[i];
        expect(currentAttr.name, expectedAttr.name.qualified);
        expect(currentAttr.localName, expectedAttr.name.local);
        expect(currentAttr.namespacePrefix, expectedAttr.name.prefix);
        expect(currentAttr.value, expectedAttr.value);
        expect(currentAttr.attributeType, expectedAttr.attributeType);
      }
      expect(event.isSelfClosing, expected.children.isEmpty && expected.isSelfClosing);
      if (!event.isSelfClosing) {
        stack.add(event);
      }
    } else if (event is XmlEndElementEvent) {
      final expected = stack.removeLast();
      expect(event.nodeType, expected.nodeType);
      expect(event.name, expected.name);
      expect(event.localName, expected.localName);
      expect(event.namespacePrefix, expected.namespacePrefix);
    } else if (event is XmlCDATAEvent) {
      final expected = nodes.removeAt(0) as XmlCDATA;
      expect(event.nodeType, expected.nodeType);
      expect(event.text, expected.text);
    } else if (event is XmlCommentEvent) {
      final expected = nodes.removeAt(0) as XmlComment;
      expect(event.nodeType, expected.nodeType);
      expect(event.text, expected.text);
    } else if (event is XmlDoctypeEvent) {
      final expected = nodes.removeAt(0) as XmlDoctype;
      expect(event.nodeType, expected.nodeType);
      expect(event.name, expected.name);
      expect(event.externalId, expected.externalId);
      expect(event.internalSubset, expected.internalSubset);
    } else if (event is XmlDeclarationEvent) {
      final expected = nodes.removeAt(0) as XmlDeclaration;
      expect(event.nodeType, expected.nodeType);
      expect(event.attributes.length, expected.attributes.length);
      for (var i = 0; i < event.attributes.length; i++) {
        final currentAttr = event.attributes[i];
        final expectedAttr = expected.attributes[i];
        expect(currentAttr.name, expectedAttr.name.qualified);
        expect(currentAttr.localName, expectedAttr.name.local);
        expect(currentAttr.namespacePrefix, expectedAttr.name.prefix);
        expect(currentAttr.value, expectedAttr.value);
        expect(currentAttr.attributeType, expectedAttr.attributeType);
      }
    } else if (event is XmlProcessingEvent) {
      final expected = nodes.removeAt(0) as XmlProcessing;
      expect(event.nodeType, expected.nodeType);
      expect(event.target, expected.target);
      expect(event.text, expected.text);
    } else if (event is XmlTextEvent) {
      final expected = nodes.removeAt(0) as XmlText;
      expect(event.nodeType, expected.nodeType);
      expect(event.text, expected.text);
      // throw StateError('Unexpected event type: $event');
    } else {
      throw StateError('Unexpected event type: $event');
    }
  }
  expect(nodes, null);
  expect(iterator.moveNext(), null);
}

void assertStreamEventInvariants(String input, XmlNode node) {
  const includedTypes = {
    XmlNodeType.CDATA,
    XmlNodeType.COMMENT,
    XmlNodeType.DECLARATION,
    XmlNodeType.DOCUMENT_TYPE,
    XmlNodeType.ELEMENT,
    XmlNodeType.PROCESSING,
    XmlNodeType.TEXT,
  };
  final parsedEvents = XmlEventDecoder().convert(input);
  final parentEvents = const XmlWithParentEvents().convert(parsedEvents);
  final nodes = node.descendants.where((node) => includedTypes.contains(node.nodeType)).toList(growable: true);
  final stack = <XmlStartElementEvent>[];
  for (final event in parentEvents) {
    if (event is XmlStartElementEvent) {
      final expected = nodes.removeAt(0) as XmlElement;
      expect(event.nodeType, expected.nodeType);
      expect(event.name, expected.name.qualified);
      expect(event.qualifiedName, expected.name.qualified);
      expect(event.localName, expected.name.local);
      expect(event.namespacePrefix, expected.name.prefix);
      expect(event.namespaceUri, expected.name.namespaceUri);
      expect(event.attributes.length, expected.attributes.length);
      for (var i = 0; i < event.attributes.length; i++) {
        final currentAttr = event.attributes[i];
        final expectedAttr = expected.attributes[i];
        expect(currentAttr.name, expectedAttr.name.qualified);
        expect(currentAttr.qualifiedName, expectedAttr.name.qualified);
        expect(currentAttr.localName, expectedAttr.name.local);
        expect(currentAttr.namespacePrefix, expectedAttr.name.prefix);
        expect(currentAttr.namespaceUri, expectedAttr.name.namespaceUri);
        expect(currentAttr.value, expectedAttr.value);
        expect(currentAttr.attributeType, expectedAttr.attributeType);
      }
      expect(event.isSelfClosing, expected.children.isEmpty && expected.isSelfClosing);
      if (!event.isSelfClosing) {
        stack.add(event);
      }
    } else if (event is XmlEndElementEvent) {
      final expected = stack.removeLast();
      expect(event.nodeType, expected.nodeType);
      expect(event.name, expected.name);
      expect(event.qualifiedName, expected.qualifiedName);
      expect(event.localName, expected.localName);
      expect(event.namespacePrefix, expected.namespacePrefix);
      expect(event.namespaceUri, expected.namespaceUri);
    } else if (event is XmlCDATAEvent) {
      final expected = nodes.removeAt(0) as XmlCDATA;
      expect(event.nodeType, expected.nodeType);
      expect(event.text, expected.text);
    } else if (event is XmlCommentEvent) {
      final expected = nodes.removeAt(0) as XmlComment;
      expect(event.nodeType, expected.nodeType);
      expect(event.text, expected.text);
    } else if (event is XmlDoctypeEvent) {
      final expected = nodes.removeAt(0) as XmlDoctype;
      expect(event.nodeType, expected.nodeType);
      expect(event.name, expected.name);
      expect(event.externalId, expected.externalId);
      expect(event.internalSubset, expected.internalSubset);
    } else if (event is XmlDeclarationEvent) {
      final expected = nodes.removeAt(0) as XmlDeclaration;
      expect(event.nodeType, expected.nodeType);
      expect(event.attributes.length, expected.attributes.length);
      for (var i = 0; i < event.attributes.length; i++) {
        final currentAttr = event.attributes[i];
        final expectedAttr = expected.attributes[i];
        expect(currentAttr.name, expectedAttr.name.qualified);
        expect(currentAttr.qualifiedName, expectedAttr.name.qualified);
        expect(currentAttr.localName, expectedAttr.name.local);
        expect(currentAttr.namespacePrefix, expectedAttr.name.prefix);
        expect(currentAttr.namespaceUri, expectedAttr.name.namespaceUri);
        expect(currentAttr.value, expectedAttr.value);
        expect(currentAttr.attributeType, expectedAttr.attributeType);
      }
    } else if (event is XmlProcessingEvent) {
      final expected = nodes.removeAt(0) as XmlProcessing;
      expect(event.nodeType, expected.nodeType);
      expect(event.target, expected.target);
      expect(event.text, expected.text);
    } else if (event is XmlTextEvent) {
      final expected = nodes.removeAt(0) as XmlText;
      expect(event.nodeType, expected.nodeType);
      expect(event.text, expected.text);
    } else {
      throw StateError('Unexpected event type: $event');
    }
  }
}

void assertStreamNodeInvariants(String input, XmlNode node) {
  final events = XmlEventCodec().decode(input);
  final nodes = XmlNodeCodec().decode(events);
  expect(nodes.length, node.children.length);
  expect(
    nodes.map((each) => each.toXmlString()).join(),
    node.children.map((each) => each.toXmlString()).join(),
  );
  for (var i = 0; i < nodes.length; i++) {
    compareNode(nodes[i], node.children[i]);
  }
}
